<?php
/**
 * Library Of Shared Code (LOSC)
 *
 * LICENSE
 *
 * This source file is subject to the New BSD License that is bundled
 * with this package in the file LICENSE.txt.
 *
 * @category   LOSC Modules
 * @package    Default
 * @subpackage Controllers
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */

/**
 * Default error controller
 * 
 * @category   LOSC Modules
 * @package    Default
 * @subpackage Controllers
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */
class Default_ErrorController extends Zym_Controller_Action_Error
{
    /**
     * Contexts
     *
     * @see Zend_Controller_Action_Helper_ContextSwitch
     * @var array
     */
    public $contexts = array(
        'bad-request'            => array('json', 'xml', 'html'),
        'forbidden'              => array('json', 'xml', 'html'),
        'not-found'              => array('json', 'xml', 'html'),
        'method-not-allowed'     => array('json', 'xml', 'html'),
        'conflict'               => array('json', 'xml', 'html'),
        'gone'                   => array('json', 'xml', 'html'),
        'unsupported-media-type' => array('json', 'xml', 'html'),
        'internal'               => array('json', 'xml', 'html'),
        'not-implemented'        => array('json', 'xml', 'html'),
        'service-unavailable'    => array('json', 'xml', 'html')
    );
    
    /**
     * Default message to use if no exception is found
     * 
     * Note that this may be changed in action methods.
     *
     * @var string
     */
    protected $_defaultMessage = 'An unknown error occured.';
    
    /**
     * Initializes error controller
     * 
     * @return void
     */
    public function init()
    {
        // determine if error/exception should be directly forwarded
        if ($error = $this->getError()) {
            $exception = $error->getException();
            
            switch (get_class($exception)) {
                case 'Losc_Exception_Http_BadRequest':
                    $this->_forward('bad-request');
                    break;
                case 'Losc_Exception_Http_Forbidden':
                    $this->_forward('forbidden');
                    break;
                case 'Losc_Exception_Http_NotFound':
                    $this->_forward('not-found');
                    break;
                case 'Losc_Exception_Http_MethodNotAllowed':
                    $allow = $exception->getAllowed();
                    $allow = 'Allow: ' . implode(', ', $allow);
                    $this->getResponse()->setRawHeader($allow);
                    $this->_forward('method-not-allowed');
                    break;
                case 'Losc_Exception_Http_Conflict':
                    $this->_forward('conflict');
                    break;
                case 'Losc_Exception_Http_Gone':
                    $this->_forward('gone');
                    break;
                case 'Losc_Exception_Http_UnsupportedMediaType':
                    $this->_forward('unsupported-media-type');
                    break;
                case 'Losc_Exception_Http_Internal':
                    $this->_forward('internal');
                    break;
                case 'Losc_Exception_Http_NotImplemented':
                    $this->_forward('not-implemented');
                    break;
                case 'Losc_Exception_Http_ServiceUnavailable':
                    $this->_forward('service-unavailable');
                    break;
            }
        }
        
        // get ContextSwitch helper
        $cs = $this->getHelper('ContextSwitch');
        
        // add html context without any suffix (use regular view scripts)
        $cs->setContext('html', array(
            'suffix' => '',
            'headers' => array('Content-Type: text/html')
        ));
        
        // callbacks to use when context switching to json/xml
        $csCallbacks = array(
            'init' => array($this, 'initContext'),
            'post' => array($this, 'postContext')
        );
        
        $cs->setCallbacks('json', $csCallbacks);
        $cs->setCallbacks('xml', $csCallbacks);
        
        // set text/javascript as content type if json format and not XHR
        if ($this->getRequest()->getParam('format') == 'json' && 
            !$this->getRequest()->isXmlHttpRequest()) {
            $cs->setHeader('json', 'Content-Type', 'text/javascript');
        }
    }
    
    /**
     * Called from the context switch helper when initing JSON/XML context
     *
     * @return void
     */
    public function initContext()
    {
        $this->getHelper('ViewRenderer')->setNoRender(true);
    }
    
    /**
     * Called from the context switch helper during post dispatch of JSON/XML
     *
     * @return void
     */
    public function postContext()
    {
        $this->render('error');
    }
    
    /**
     * Store exception and message in view after dispatch
     *
     * @return void
     */
    public function postDispatch()
    {
        // store error data in view
        if (!isset($this->view->message)) {
            if ($error = $this->getError()) {
                $e = $error->getException();
                $this->view->exception = get_class($e);
                $this->view->code = $e->getCode();
                
                if ($e instanceof Losc_Exception) {
                    $this->view->message = $e->getLocalizedMessage();
                } else {
                    $this->view->message = $e->getMessage();
                }
            } else {
                $t = $this->getHelper('Translator');
                $this->view->exception = '&lt;unknown&gt;';
                $this->view->code = 0;
                $this->view->message = $t->_($this->_defaultMessage);
            }
        }
    }
    
    /**#@+
     * 4xx Client Error
     * 
     * The request contains bad syntax or cannot be fulfilled.
     * 
     * The 4xx class of status code is intended for cases in which the client 
     * seems to have erred. Except when responding to a HEAD request, the server
     * should include an entity containing an explanation of the error 
     * situation, and whether it is a temporary or permanent condition. These 
     * status codes are applicable to any request method. User agents should 
     * display any included entity to the user. These are typically the most 
     * common codes encountered while online.
     */
    
    /**
     * 400 Bad Request
     * 
     * The request could not be understood by the server due to malformed 
     * syntax. The client SHOULD NOT repeat the request without modifications. 
     *
     * @return void
     */
    public function badRequestAction()
    {
        $this->_defaultMessage = 'The request contains bad syntax or cannot be fulfilled.';
        $this->getResponse()->setRawHeader('HTTP/1.1 400 Bad Request');
    }
    
    /**
     * 403 Forbidden
     * 
     * The request was a legal request, but the server is refusing to respond 
     * to it. Unlike a 401 Unauthorized response, authenticating will make no 
     * difference.
     *
     * @return void
     */
    public function forbiddenAction()
    {
        $this->_defaultMessage = 'Access to the resource is forbidden.';
        $this->getResponse()->setRawHeader('HTTP/1.1 403 Forbidden');
    }
    
    /**
     * 404 Not Found
     * 
     * The requested resource could not be found.
     *
     * @return void
     */
    public function notFoundAction()
    {
        $this->_defaultMessage = 'The requested resource cannot be found.';
        $this->getResponse()->setRawHeader('HTTP/1.1 404 Not Found');
    }
    
    /**
     * 405 Method Not Allowed
     * 
     * A request was made of a resource using a request method not supported by 
     * that resource; for example, using GET on a form which requires data to 
     * be presented via POST, or using PUT on a read-only resource.
     *
     * @return void
     */
    public function methodNotAllowedAction()
    {
        $this->_defaultMessage = 'Request method not allowed.';
        $this->getResponse()->setRawHeader('HTTP/1.1 405 Method Not Allowed');
    }
    
    /**
     * 409 Conflict
     * 
     * The request could not be completed due to a conflict with the current 
     * state of the resource. This code is only allowed in situations where it 
     * is expected that the user might be able to resolve the conflict and 
     * resubmit the request. The response body SHOULD include enough information
     * for the user to recognize the source of the conflict. Ideally, the 
     * response entity would include enough information for the user or user 
     * agent to fix the problem; however, that might not be possible and is not
     * required.
     * 
     * Conflicts are most likely to occur in response to a PUT request. For 
     * example, if versioning were being used and the entity being PUT included
     * changes to a resource which conflict with those made by an earlier 
     * (third-party) request, the server might use the 409 response to indicate
     * that it can't complete the request. In this case, the response entity 
     * would likely contain a list of the differences between the two versions
     * in a format defined by the response Content-Type. 
     *
     * @return void
     */
    public function conflictAction()
    {
        $this->_defaultMessage = 'The request could not be completed due to a '
                          . 'conflict with the current state of the resource.';
        $this->getResponse()->setRawHeader('HTTP/1.1 409 Conflict');
    }
    
    /**
     * 410 Gone
     * 
     * The requested resource is no longer available at the server and no 
     * forwarding address is known. This condition is expected to be considered
     * permanent. Clients with link editing capabilities SHOULD delete 
     * references to the Request-URI after user approval. If the server does not
     * know, or has no facility to determine, whether or not the condition is 
     * permanent, the status code 404 (Not Found) SHOULD be used instead. This 
     * response is cacheable unless indicated otherwise.
     * 
     * The 410 response is primarily intended to assist the task of web 
     * maintenance by notifying the recipient that the resource is intentionally
     * unavailable and that the server owners desire that remote links to that 
     * resource be removed. Such an event is common for limited-time, 
     * promotional services and for resources belonging to individuals no longer
     * working at the server's site. It is not necessary to mark all permanently
     * unavailable resources as "gone" or to keep the mark for any length of 
     * time -- that is left to the discretion of the server owner. 
     *
     * @return void
     */
    public function goneAction()
    {
        $this->_defaultMessage = 'The requested resource is no longer '
                               . 'available at the server.';
        $this->getResponse()->setRawHeader('HTTP/1.1 410 Gone');
    }
    
    /**
     * 415 Unsupported Media Type
     * 
     * The server is refusing to service the request because the entity of the 
     * request is in a format not supported by the requested resource for the 
     * requested method. 
     *
     * @return void
     */
    public function unsupportedMediaTypeAction()
    {
        $this->_defaultMessage = 'Media type not supported.';
        $this->getResponse()->setRawHeader('HTTP/1.1 415 Unsupported Media Type');
    }
    
    /**#@+
     * 5xx Server Error
     * 
     * The server failed to fulfill an apparently valid request.
     * 
     * Response status codes beginning with the digit "5" indicate cases in 
     * which the server is aware that it has errored or is incapable of 
     * performing the request. Except when responding to a HEAD request, the 
     * server should include an entity containing an explanation of the error 
     * situation, and whether it is a temporary or permanent condition. User 
     * agents should display any included entity to the user. These response 
     * codes are applicable to any request method.
     */
    
    /**
     * 500 Internal Server Error
     * 
     * The server encountered an unexpected condition which prevented it from
     * fulfilling the request. 
     *
     * @return void
     */
    public function internalAction()
    {
        $this->_defaultMessage = 'An internal server error occured.';
        $this->getResponse()->setRawHeader('HTTP/1.1 500 Internal Server Error');
    }
    
    /**
     * 501 Not Implemented
     * 
     * The server does not support the functionality required to fulfill the 
     * request. This is the appropriate response when the server does not 
     * recognize the request method and is not capable of supporting it for any
     * resource. 
     *
     * @return void
     */
    public function notImplementedAction()
    {
        $this->_defaultMessage = 'The server does not support the functionality'
                               . ' required to fulfill the request.';
        $this->getResponse()->setRawHeader('HTTP/1.1 501 Not Implemented');
    }
    
    /**
     * 503 Service Unavailable
     * 
     * The server is currently unable to handle the request due to a temporary
     * overloading or maintenance of the server. The implication is that this is
     * a temporary condition which will be alleviated after some delay. If 
     * known, the length of the delay MAY be indicated in a Retry-After header. 
     * If no Retry-After is given, the client SHOULD handle the response as it 
     * would for a 500 response. 
     *
     * @return void
     */
    public function serviceUnavailableAction()
    {
        $this->_defaultMessage = 'Service is currently unavailable.';
        $this->getResponse()->setRawHeader('HTTP/1.1 503 Service Unavailable');
    }
}
